"""
会议预定未开会违约检查释放会议室资源
会议提前结束检查释放会议室资源
"""

from string import Template
from datetime import timedelta
from typing import Optional, Tuple, List, Union
import time as lib_time
import asyncio
from concurrent.futures import ThreadPoolExecutor, Future, ALL_COMPLETED, wait as wait_futures

from django.db import transaction
from django.utils import timezone as dj_timezone
from django.db.models import QuerySet

from apps.app_users.models import Email
from apps.app_users.email_sender import EmailConfig
from apps.app_tx_meeting.models import MeetingReservation, ReservationNoShow
from apps.app_tx_meeting.managers import ReservationNoShowManager, NoShowWhitelistManager, tx_meeting_configs
from apps.app_tx_meeting.workers.room_reserve_release import RoomReserveReleaseWorker
from apps.app_tx_meeting.builders import build_tx_meeting_client
from apps.app_tx_meeting.tx_clients import inputs, outputs
from apps.app_tx_meeting.tx_clients.client import TxMeetingClient
from apps.app_tx_meeting.helpers import TxMeetingEmailHandle
from apps.app_tx_meeting.notice_templates import NO_SHOW_NOTICE_TEMPLATE, build_utc8_time_range_content


class MeetingNoShowReleaseWorker:
    """
    会议预定未开会违约检查释放会议室资源
    会议提前结束检查释放会议室资源
    """
    @staticmethod
    def get_now_ts() -> int:
        return int(lib_time.time())

    def __init__(self, check_noshow: bool = True):
        """
        check_noshow: 是否做违约检查工作
        """
        self.room_release_worker = RoomReserveReleaseWorker()
        self.pool_executor = ThreadPoolExecutor()
        self.noshow_email_template = Template(NO_SHOW_NOTICE_TEMPLATE)
        self.check_noshow = check_noshow
        self.tx_client = build_tx_meeting_client()
        self.noshow_notice_enabled = tx_meeting_configs.to_bool(
            tx_meeting_configs.get(tx_meeting_configs.ConfigName.NO_SHOW_NOTICE_ENABLED.value)
        )

    def run(self):
        """
        注意：每个新的实例对象的此方法只能执行一次，因为执行完 线程池 会shutdown
        """
        now_ts = self.get_now_ts()
        reservations = self.get_live_reservations(now_ts=now_ts)
        reservations_with_status = self.get_meeting_status_of_reservations(reservations=reservations)
        email_futures = self.handle_noshow_early_release(reservations_with_status=reservations_with_status)

        # 等待违约邮件任务完成，再重试失败的，防止重复发送邮件
        if email_futures:
            wait_futures(email_futures, return_when=ALL_COMPLETED)

        # 重试发送失败的通知邮件
        self.retry_send_failed_emails()

        # 等待所有邮件任务完成
        self.pool_executor.shutdown(wait=True)

        return {
        }

    @staticmethod
    def get_live_reservations(now_ts: int) -> QuerySet:
        """
        查询在进行中的会议预定
        """
        qs = MeetingReservation.objects.select_related('user').filter(
            status=MeetingReservation.RoomReserveStatus.RESERVED.value,
            room_reserve_status__in=[
                MeetingReservation.RoomReserveStatus.RESERVED.value,  # 已留会议室资源的
                MeetingReservation.RoomReserveStatus.UNRELEASED.value,  # 议室资源释放失败的
            ],
            start_time__lte=now_ts - 60 * 30,  # 开始时间30分钟后
            end_time__gt=now_ts + 60 * 5,  # 会议预定结束时间前5分钟之前
        )
        return qs.order_by('creation_time')

    @staticmethod
    async def get_meeting_detail(
            tx_client: TxMeetingClient, reservation: MeetingReservation
    ) -> Tuple[MeetingReservation, outputs.GetMeetingDetailOutput]:
        detail_output = await tx_client.async_get_meeting_detail(
            params=inputs.GetMeetingDetailInput(
                meeting_id=reservation.tx_meeting_id, instanceid=5
            )
        )
        return reservation, detail_output

    @staticmethod
    async def do_async_requests(tasks):
        return await asyncio.gather(*tasks, return_exceptions=True)

    def get_meeting_status_of_reservations(
            self, reservations
    ) -> List[Union[Tuple[MeetingReservation, outputs.GetMeetingDetailOutput], Exception]]:
        tx_client = self.tx_client
        tasks = [self.get_meeting_detail(tx_client=tx_client, reservation=res) for res in reservations]
        if not tasks:
            return []

        results = asyncio.run(self.do_async_requests(tasks))
        return results

    def handle_noshow_early_release(
            self,
            reservations_with_status: List[
                Union[Tuple[MeetingReservation, outputs.GetMeetingDetailOutput], Exception]
            ]
    ) -> List[Future]:
        """
        会议预定违约检查处理，已结束会议提前释放资源
        """
        email_futures = []
        email_conf = TxMeetingEmailHandle().get_email_configs()
        for item in reservations_with_status:
            try:
                future = self.handle_one_reservation(item, email_conf=email_conf)
                if future:
                    email_futures.append(future)
            except Exception as e:
                print(e)
                pass

        return email_futures

    def handle_one_reservation(
            self, data_or_err: Union[Tuple[MeetingReservation, outputs.GetMeetingDetailOutput], Exception],
            email_conf: EmailConfig
    ) -> Optional[Future]:
        if isinstance(data_or_err, Exception):
            return

        email_future = None
        reservation, detail_output = data_or_err
        if self.check_noshow:
            is_noshow, email_future = self.do_noshow(
                reservation=reservation, detail_output=detail_output, email_conf=email_conf)
            # 不是违约或不确定违约，需要检查资源提前释放
            if not is_noshow:
                self.do_early_release(reservation=reservation, detail_output=detail_output)
        else:
            self.do_early_release(reservation=reservation, detail_output=detail_output)

        return email_future

    def do_noshow(self, reservation, detail_output, email_conf: EmailConfig) -> Tuple[Optional[bool], Optional[Future]]:
        """
        违约处理
        :return:(
            bool,   # 是否违约；None(无法判定)，True(违约)，False(未违约)
            Future
        )
        """
        email_future = None
        is_noshow, status = self.is_noshow(reservation=reservation, detail_output=detail_output)
        if not is_noshow:
            return is_noshow, None

        # 进一步确认是否违约，查询参会人数
        try:
            particpants_num = self.get_meeting_particpants_num(meeting_id=reservation.tx_meeting_id)
        except Exception as e:
            return None, None

        if particpants_num is None:
            return None, None   # 不确定是否违约
        elif particpants_num > 0:
            return False, None  # 未违约

        noshow_username = reservation.tx_meeting_userid
        is_whitelist_user = NoShowWhitelistManager.is_user_in_whitelist(noshow_username)
        if is_whitelist_user:
            noshow_status = ReservationNoShow.ValidStatus.EXEMPT.value
        else:
            noshow_status = ReservationNoShow.ValidStatus.VALID.value

        # 违约
        with transaction.atomic():
            new_reservation = MeetingReservation.objects.select_for_update().get(id=reservation.id)
            new_reservation.status = MeetingReservation.Status.NO_SHOW.value
            new_reservation.save(update_fields=['status', 'update_time'])
            noshow = ReservationNoShowManager.create_reservation_noshow(
                reservation=reservation, username=noshow_username,
                user_nickname=reservation.tx_user_nick_name, remarks=f'{status}, 参会人数{particpants_num}',
                valid_status=noshow_status
            )

        # 如果开启违约邮件通知
        if self.noshow_notice_enabled:
            if not is_whitelist_user:
                # 邮件通知
                try:
                    email = self.create_noshow_email(tx_meeting=new_reservation, noshow=noshow, email_conf=email_conf)
                    if email:
                        email_future = self.pool_executor.submit(self.task_send_email, email=email)
                except Exception as e:
                    pass

        # 释放room资源
        self.room_release_worker.release_meeting_room(reservation=new_reservation, log_msg=f'违约，status={status}')

        return True, email_future

    def do_early_release(self, reservation, detail_output):
        """
        处理提前释放资源
        """
        can, status = self.can_early_release(reservation=reservation, detail_output=detail_output)
        if can:
            # 释放room资源
            self.room_release_worker.release_meeting_room(
                reservation=reservation, early_release=True, log_msg=f'status={status}')

    @staticmethod
    def is_noshow(
            reservation: MeetingReservation, detail_output: outputs.GetMeetingDetailOutput
    ) -> Tuple[Optional[bool], str]:
        """
        判定是否违约

        :return: is_noshow, status
            True    # 违约
            False   # 未违约
            None    # 无法判定
        """
        now_ts = MeetingNoShowReleaseWorker.get_now_ts()
        # 预定开始时间30分钟后至在预定结束时间之前，不再此时间内不做判定
        if not ((reservation.start_time + 60 * 30) < now_ts < reservation.end_time):
            return None, ''

        # 未成功查询到会议信息
        if not detail_output.ok or not detail_output.data:
            return None, ''

        if not detail_output.data.meeting_info_list:
            return None, ''

        detail = detail_output.data.meeting_info_list[0]
        if detail.meeting_id != reservation.tx_meeting_id:
            return None, ''

        # 0：一次性会议; 1：周期性会议
        if 0 != detail.meeting_type:
            return None, ''

        # 当前会议状态：
        # MEETING_STATE_INVALID：非法或未知的会议状态，错误状态。
        # MEETING_STATE_INIT：会议待开始。会议预定到预定结束时间前，会议尚无人进会。
        # MEETING_STATE_CANCELLED：会议已取消。主持人主动取消会议，待开始的会议才能取消，且取消的会议无法再进入。
        # MEETING_STATE_STARTED：会议已开始。会议中有人则表示会议进行中。
        # MEETING_STATE_ENDED：会议已删除。1、会议已过预定结束时间且尚无人进会时，主持人删除会议，已删除的会议无法再进入。2、删除了正常结束的会议
        # MEETING_STATE_NULL：会议无状态。1、会议已过预定结束时间，会议尚无人进会; 2、正常结束的会议
        # MEETING_STATE_RECYCLED：会议已回收。会议已过预定开始时间30天，则会议号将被后台回收，无法再进入。
        if detail.status in ['MEETING_STATE_INIT', 'MEETING_STATE_CANCELLED']:
            return True, detail.status

        if detail.status in ['MEETING_STATE_STARTED']:
            return False, detail.status

        return None, ''

    def get_meeting_particpants_num(self, meeting_id: str) -> Optional[int]:
        """
        会议参会人员数量查询
        :return:
            None # 未成功查询到数据
        """
        output_obj = self.tx_client.get_meeting_participants(
            params=inputs.GetMeetingParticipantsInput(
                meeting_id=meeting_id, userid=self.tx_client.operator_id, size=6
            ))

        # 未成功查询到数据
        if not output_obj.ok or not output_obj.data:
            return None

        return output_obj.data.total_count

    @staticmethod
    def can_early_release(
            reservation: MeetingReservation, detail_output: outputs.GetMeetingDetailOutput
    ) -> Tuple[Optional[bool], str]:
        """
        判定是否可以提前释放会议室资源

        :return: bool, status
            True    # 可以
            False   # 不可以
            None    # 无法判定
        """
        now_ts = MeetingNoShowReleaseWorker.get_now_ts()
        # 预定开始时间30分钟后至在预定结束时间之前，不再此时间内不做判定
        if not ((reservation.start_time + 60 * 30) < now_ts < reservation.end_time):
            return None, ''

        # 未成功查询到会议信息
        if not detail_output.ok or not detail_output.data:
            return None, ''

        if not detail_output.data.meeting_info_list:
            return None, ''

        detail = detail_output.data.meeting_info_list[0]
        if detail.meeting_id != reservation.tx_meeting_id:
            return None, ''

        # 0：一次性会议; 1：周期性会议
        if detail.meeting_type not in [0, 1]:
            return None, ''

        # 当前会议状态：
        # MEETING_STATE_INVALID：非法或未知的会议状态，错误状态。
        # MEETING_STATE_INIT：会议待开始。会议预定到预定结束时间前，会议尚无人进会。
        # MEETING_STATE_CANCELLED：会议已取消。主持人主动取消会议，待开始的会议才能取消，且取消的会议无法再进入。
        # MEETING_STATE_STARTED：会议已开始。会议中有人则表示会议进行中。
        # MEETING_STATE_ENDED：会议已删除。1、会议已过预定结束时间且尚无人进会时，主持人删除会议，已删除的会议无法再进入。2、删除了正常结束的会议
        # MEETING_STATE_NULL：会议无状态。1、会议已过预定结束时间，会议尚无人进会; 2、正常结束的会议
        # MEETING_STATE_RECYCLED：会议已回收。会议已过预定开始时间30天，则会议号将被后台回收，无法再进入。

        if detail.status in [
            'MEETING_STATE_INIT', # 周期会议时，可能未开始，也可能已结束
            'MEETING_STATE_CANCELLED', 'MEETING_STATE_ENDED',
            'MEETING_STATE_NULL', 'MEETING_STATE_RECYCLED'
        ]:
            return True, detail.status

        if detail.status in ['MEETING_STATE_STARTED']:
            return False, detail.status

        return None, ''

    @staticmethod
    def build_notice_email_message(template: Template, tx_meeting: MeetingReservation) -> str:
        """
        通知内容构建
        """

        time_format = build_utc8_time_range_content(
            start_timestamp=tx_meeting.start_time, end_timestamp=tx_meeting.end_time)

        content = template.safe_substitute(
            meeting_userid=tx_meeting.tx_meeting_userid,
            meeting_subject=tx_meeting.subject,
            meeting_time=time_format,
            meeting_code=tx_meeting.tx_meeting_code
        )

        return content

    def create_noshow_email(self, tx_meeting: MeetingReservation, noshow: ReservationNoShow, email_conf) -> Email:
        """
        发送腾讯会议违约通知邮件
        """

        message = self.build_notice_email_message(template=self.noshow_email_template, tx_meeting=tx_meeting)

        try:
            with transaction.atomic():
                email = TxMeetingEmailHandle.save_email(
                    subject='科技云会（腾讯版）未参会通知', receivers=[tx_meeting.tx_meeting_userid],
                    message=message, tag=Email.Tag.TX_NOSHOW.value,
                    email_conf=email_conf)

                noshow.email_notice = email
                noshow.save(update_fields=['email_notice'])
        except Exception as e:
            raise e

        return email

    @staticmethod
    def get_need_retry_failed_emails():
        """ 查询最近发送失败需要重试通知邮件 """

        dt_hours_ago = dj_timezone.now() - timedelta(hours=1)
        return Email.objects.filter(
            tag=Email.Tag.TX_NOSHOW.value,
            send_time__gte=dt_hours_ago,
            status__in=[Email.Status.WAIT.value, Email.Status.FAILED.value],
            success_time__isnull=True
        ).order_by('-send_time')[:200]

    @staticmethod
    def task_send_email(email: Email, email_conf: EmailConfig = None):
        TxMeetingEmailHandle.send_email(email=email, email_conf=email_conf)

    def retry_send_failed_emails(self):
        email_qs = self.get_need_retry_failed_emails()
        emails = list(email_qs)
        if not emails:
            return

        email_conf = TxMeetingEmailHandle().get_email_configs()
        with ThreadPoolExecutor() as executor:
            for email in emails:
                executor.submit(self.task_send_email, email=email, email_conf=email_conf)
